﻿using System;
using System.Xml;
using Notoric.Model.Internal;
using Notoric.Model.Packaging;
using Notoric.Model.Validation;

namespace Notoric.Model.Elements.CWNotation
{
    /// <summary>
    /// Represents an accidental.
    /// </summary>
    /// <remarks>
    /// <para>
    /// An accidental sign raises or lowers the following <see cref="Note"/> from its normal pitch, usually by a semitone, 
    /// although microtonal music may use "fractional" accidental signs, and one occasionally sees double sharps or flats, 
    /// which raise or lower the indicated note by a whole tone (though their most common usage is in keys in which the altered <see cref="Note"/> 
    /// is already raised or lowered by the key signature, so the altered note is only a half step from its usual pitch in that key). 
    /// Accidentals apply within the measure and octave in which they appear, unless canceled by another accidental sign, or tied into a following measure.
    /// </para>
    /// <para>
    /// The modern accidental signs derive from the round and square small letter b used in Gregorian chant manuscripts to signify the two pitches of B, 
    /// the only <see cref="Note"/> that could be altered. The round b became the flat sign, while the square b diverged into the sharp and natural signs.
    /// </para>
    /// <para>
    /// In most cases, a sharp raises the pitch of a note one semitone while a flat lowers it a semitone. 
    /// A natural is used to cancel the effect of a flat or sharp. This system of accidentals operates in conjunction with the key signature, 
    /// whose effect continues throughout an entire piece, unless canceled by another key signature. An accidental can also be used to cancel 
    /// or reinstate the flats or sharps of the key signature.
    /// </para>
    /// <para>
    /// Since about 1700, accidentals have been understood to continue for the remainder of the measure in which they occur, 
    /// so that a subsequent note on the same staff position is still affected by that accidental, unless marked as an accidental 
    /// on its own. Notes on other staff positions, including those an octave away, are unaffected. 
    /// Once a barline is passed, the effect of the accidental ends, except when a note affected by an accidental is tied to the same note 
    /// across a barline. Sight reading music at speed can be mentally (and physically) taxing, so where an editor or engraver sees a possible 
    /// opportunity for confusion, a courtesy or cautionary accidental may be placed by a note whose pitch is, strictly speaking, already given by the key signature.
    /// </para>
    /// <para>
    /// <i>Source: <a href="http://en.wikipedia.org/wiki/Accidental_(music)">Wikipedia</a></i>
    /// </para>
    /// </remarks>
    /// <seealso cref="Note"/>
    /// <seealso cref="KeySignature"/>
    public class Accidental : ModelElement
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="Accidental"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="Note"/>
        /// </para>
        /// <para>
        /// The note that directly owns this element.
        /// </para>
        /// </param>
        /// <param name="type">
        /// <para>
        /// Type: <see cref="AccidentalType"/>
        /// </para>
        /// <para>
        /// The type of the accidental.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="packagePart"/> or <paramref name="owner"/> is <see langword="null"/>.
        /// </exception>
        internal Accidental(XmlPackagePart packagePart, Note owner, AccidentalType type)
            : base(packagePart, owner)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");

            typeAttribute.Value = type.ToString();
            positionAttribute.Value = (0.0).ToString();
            courtesyAttribute.Value = false.ToString();
        }

        /// <summary>
        /// Creates a new <see cref="Accidental"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="ModelElement"/>
        /// </para>
        /// <para>
        /// The model element that directly owns this element.
        /// </para>
        /// </param>
        /// <param name="element">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The XML element that holds the data of this object.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        internal Accidental(XmlPackagePart packagePart, ModelElement owner, XmlElement element)
            : base(packagePart, owner, element)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");

            courtesyAttribute = element.Attributes[PackageVocabulary.IsCourtesyAttribute];
            positionAttribute = element.Attributes[PackageVocabulary.PositionAttribute];
            typeAttribute = element.Attributes[PackageVocabulary.AccidentalTypeAttribute];
        }

        #endregion

        #region ModelElement Members

        /// <inheritdoc/>
        protected override XmlElement CreateObjectElement(XmlDocument document)
        {
            var element = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.AccidentalElementName,
                PackageVocabulary.PackageNamespaceUri
                );

            courtesyAttribute = document.CreateAttribute(PackageVocabulary.IsCourtesyAttribute);
            positionAttribute = document.CreateAttribute(PackageVocabulary.PositionAttribute);
            typeAttribute = document.CreateAttribute(PackageVocabulary.AccidentalTypeAttribute);

            element.Attributes.Append(courtesyAttribute);
            element.Attributes.Append(positionAttribute);
            element.Attributes.Append(typeAttribute);

            return element;
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Converts a given accidental type to the corresponding accidental value.
        /// </summary>
        /// <param name="type">
        /// <para>
        /// Type: <see cref="AccidentalType"/>
        /// </para>
        /// <para>
        /// The converted accidental type.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The accidental value.
        /// The value ranges from -1.0 to 1.0 giving the number of tones by which
        /// the pitch of a note should be raised or lowered.
        /// </para>
        /// </returns>
        public static double AccidentalTypeToValue(AccidentalType type)
        {
            switch (type)
            {
                case AccidentalType.DoubleFlat:
                    return -1.0;
                case AccidentalType.Flat:
                    return -0.5;
                case AccidentalType.Sharp:
                    return 0.5;
                case AccidentalType.DoubleSharp:
                    return 1.0;
                default:
                    return 0.0;
            }
        }

        /// <summary>
        /// Converts a given accidental value to the corresponding accidental type.
        /// </summary>
        /// <param name="accidentalValue">
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The converted accidental value.
        /// The value ranges from -1.0 to 1.0 giving the number of tones
        /// by which the pitch of a note should be raised or lowered.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="AccidentalType"/>
        /// </para>
        /// <para>
        /// The accidental type that corresponds to the given accidental value.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentException">
        /// The value does not correspond to any <see cref="AccidentalType"/>.
        /// </exception>
        public static AccidentalType AccidentalValueToType(double accidentalValue)
        {
            if (accidentalValue == -1.0)
                return AccidentalType.DoubleFlat;
            else if (accidentalValue == -0.5)
                return AccidentalType.Flat;
            else if (accidentalValue == 0.0)
                return AccidentalType.Neutral;
            else if (accidentalValue == 0.5)
                return AccidentalType.Sharp;
            else if (accidentalValue == 1.0)
                return AccidentalType.DoubleSharp;

            throw new ArgumentException("Unknown accidental value!", "accidentalValue");
        }

        /// <summary>
        /// Gives the type of the neutral sign (simple or double) that will cancel the given accidental.
        /// </summary>
        /// <param name="type">
        /// <para>
        /// Type: <see cref="AccidentalType"/>
        /// </para>
        /// <para>
        /// The type of the canceled accidental.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="AccidentalType"/>
        /// </para>
        /// <para>
        /// The sign that will cancel the accidental.
        /// <list type="table">
        /// <listheader>
        /// <term>
        ///     Canceled Accidental
        /// </term>
        /// <description>
        ///     Return Value
        /// </description>
        /// </listheader>
        /// <item>
        /// <term>
        ///     <see cref="AccidentalType.DoubleSharp"/>, <see cref="AccidentalType.DoubleFlat"/>
        /// </term>
        /// <description>
        ///     <see cref="AccidentalType.DoubleNeutral"/>
        /// </description>
        /// </item>
        /// <item>
        /// <term>
        ///     <see cref="AccidentalType.Sharp"/>, <see cref="AccidentalType.Flat"/>
        /// </term>
        /// <description>
        ///     <see cref="AccidentalType.Neutral"/>
        /// </description>
        /// </item>
        /// <item>
        /// <term>
        ///     <see cref="AccidentalType.Neutral"/>, <see cref="AccidentalType.DoubleNeutral"/>
        /// </term>
        /// <description>
        ///     <see cref="AccidentalType.None"/>
        /// </description>
        /// </item>
        /// </list>
        /// </para>
        /// </returns>
        public static AccidentalType CancelAccidental(AccidentalType type)
        {
            switch (type)
            {
                case AccidentalType.DoubleSharp:
                case AccidentalType.DoubleFlat:
                    return AccidentalType.DoubleNeutral;
                case AccidentalType.Flat:
                case AccidentalType.Sharp:
                    return AccidentalType.Neutral;
                default:
                    return AccidentalType.None;
            }
        }

        /// <summary>
        /// Determines if this accidental collide with another given one.
        /// </summary>
        /// <remarks>
        /// This method only computes collision for accidentals that are both 
        /// on the same <see cref="Staff"/>. Otherwise it returns <see langword="false"/>.
        /// </remarks>
        /// <param name="other">
        /// <para>
        /// Type: <see cref="Accidental"/>
        /// </para>
        /// <para>
        /// The accidental with which the collision is checked.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Boolean"/>
        /// </para>
        /// <para>
        /// <see langword="true"/> if the accidentals collide, <see langword="false"/> otherwise.
        /// </para>
        /// </returns>
        public bool CollideWith(Accidental other)
        {
            if (other == null)
                return false;
            if (Type == AccidentalType.None || other.Type == AccidentalType.None)
                return false;
            if (other.FindOwner<Staff>() != FindOwner<Staff>())
                return false;

            var myOwnerNote = FindOwner<Note>();
            var hisOwnerNote = other.FindOwner<Note>();
            
            Note lowerNote, upperNote;
            if (myOwnerNote.StaffPosition > hisOwnerNote.StaffPosition)
            {
                lowerNote = hisOwnerNote;
                upperNote = myOwnerNote;
            }
            else
            {
                lowerNote = myOwnerNote;
                upperNote = hisOwnerNote;
            }

            bool verticalCollision = lowerNote.StaffPosition + lowerNote.Accidental.SpaceAbove > upperNote.StaffPosition - upperNote.Accidental.SpaceBellow;

            double distance = Math.Abs(Position + FindOwner<Chord>().Position - other.Position - other.FindOwner<Chord>().Position);
            double limit = (Position < other.Position ? Width : other.Width);

            bool horizontalCollision = distance < limit;

            return verticalCollision && horizontalCollision;
        }

        /// <summary>
        /// Computes the offset of two accidentals.
        /// </summary>
        /// <remarks>
        /// This method helps the layouting of the accidentals in chord and polyphonic compositions.
        /// It computes the offset that is necessary between two accidentals so that they do not collide.
        /// This does not mean that their bounding boxes cannot be superposed, but the accidentals will
        /// remain neat.
        /// </remarks>
        /// <param name="other">
        /// <para>
        /// Type: <see cref="Accidental"/>
        /// </para>
        /// <para>
        /// The accidental from which the offset is being computed.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The offset (in centimeters) of this accidental from the <paramref name="other"/> so that
        /// the two accidentals would not colide.
        /// </para>
        /// </returns>
        public double GetAccidentalsOffset(Accidental other)
        {
            if (other == null)
                throw new ArgumentNullException("other");
            if (other.Type == AccidentalType.None || Type == AccidentalType.None)
                return 0.0;
            if (other.FindOwner<Staff>() != FindOwner<Staff>())
                throw new ArgumentException("The other accidental is not drawn on the same staff!", "other");
            if (other == this)
                return 0.0;

            var lower = this;
            var upper = other;
            var posLower = FindOwner<Note>().StaffPosition;
            var posUpper = other.FindOwner<Note>().StaffPosition;
            var spaceSize = FindOwner<Staff>().SpaceSize;

            if (posLower > posUpper)
            {
                var tmpPos = posLower;
                posLower = posUpper;
                posUpper = tmpPos;

                lower = other;
                upper = this;
            }

            if (posLower + lower.SpaceAbove < posUpper - upper.SpaceBellow)
                return 0.0;

            var distance = posUpper - posLower;
            double offset;

            switch (lower.Type)
            {
                case AccidentalType.Flat:
                    if (distance >= 4)
                        offset = - 0.25 * spaceSize;
                    else
                        offset = - spaceSize;
                    break;
                case AccidentalType.Sharp:
                    switch (upper.Type)
                    {
                        case AccidentalType.Sharp:
                            if (distance >= 5)
                                offset = - 0.5 * spaceSize;
                            else
                                offset = - spaceSize;
                            break;
                        case AccidentalType.Neutral:
                            if (distance >= 4)
                                offset = - 0.25 * spaceSize;
                            else
                                offset = - spaceSize;
                            break;
                        case AccidentalType.DoubleNeutral:
                            if (distance >= 4)
                                offset = - 0.5 * spaceSize;
                            else
                                offset = - spaceSize;
                            break;
                        default:
                            offset = - spaceSize;
                            break;
                    }
                    break;
                case AccidentalType.Neutral:
                    switch (other.Type)
                    {
                        case AccidentalType.Neutral:
                        case AccidentalType.Flat:
                            if (distance >= 4)
                                offset = - 0.25 * spaceSize;
                            else
                                offset = - spaceSize;
                            break;
                        case AccidentalType.Sharp:
                            if (distance >= 5)
                                offset = - 0.5 * spaceSize;
                            else
                                offset = - spaceSize;
                            break;
                        default:
                            if (distance >= 4)
                                offset = - 0.25 * spaceSize;
                            else
                                offset = - spaceSize;
                            break;
                    }
                    break;
                case AccidentalType.DoubleFlat:
                case AccidentalType.DoubleNeutral:
                    offset = - 2 * spaceSize;
                    break;
                default:
                    offset = - spaceSize;
                    break;
            }

            if (lower == this)
                return offset;

            return -offset;
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets the effective type size of the symbol.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The effective type size (in centimeters) of the symbol.
        /// </para>
        /// </value>
        public double EffectiveTypeSize
        {
            get
            {
                return FindOwner<Note>().EffectiveTypeSize;
            }
        }

        /// <summary>
        /// Gets or sets the value indicating whether this accidental is a courtesy accidental.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Boolean"/>
        /// </para>
        /// <para>
        /// Indicates whether this accidental is a courtesy accidental or not. (Default is <see langword="false"/>).
        /// </para>
        /// <para>
        /// Although a <see cref="Barline"/> is nowadays understood to cancel the effect of an <see cref="Accidental"/> 
        /// (except for a tied <see cref="Note"/>), often publishers will use a courtesy accidental (also referred to 
        /// as a cautionary accidental or a reminder accidental) as a reminder of the correct pitch if the same note 
        /// occurs in the following measure. 
        /// This usage varies, although a few situations are construed to require a courtesy accidental, such as
        /// <list type="bullet">
        /// <item>
        /// when the first <see cref="Note"/> in a measure is one which had had an <see cref="Accidental"/> applied in the previous measure
        /// </item>
        /// <item>
        /// after a tie carries an <see cref="Accidental"/> across a <see cref="Barline"/>, when the same <see cref="Note"/> appears again in the subsequent measure.
        /// </item>
        /// </list>
        /// Other uses are inconsistently applied.<br/>
        /// Courtesy accidentals are sometimes enclosed in parentheses to emphasize their nature as reminders.<br/>
        /// Publishers of jazz music and some atonal music sometimes eschew all courtesy accidentals.<br/>
        /// </para>
        /// </value>
        public bool IsCourtesy
        {
            get { return bool.Parse(courtesyAttribute.Value); }
            set
            {
                courtesyAttribute.Value = value.ToString();

                RaisePropertyChanged("IsCourtesy");
            }
        }

        /// <summary>
        /// Gets or sets the horizontal position of the accidental.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The horizontal offset (in centimeters) of the accidental from the stem of its chord.
        /// </para>
        /// </value>
        /// <exception cref="ValidationException">
        /// The <paramref name="value"/> is <see cref="Double.NaN"/>.
        /// </exception>
        /// TODO: Write the validation rules.
        public double Position
        {
            get { return double.Parse(positionAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat); }
            set
            {
                ValidateAndThrow("Position", value);

                string posStr = value.ToString(PackageVocabulary.PackageCulture.NumberFormat);
                if (positionAttribute.Value != posStr)
                {
                    positionAttribute.Value = posStr;

                    RaisePropertyChanged("Position");
                }
            }
        }

        /// <summary>
        /// Gets the space occupied by this accidental above its position.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The space (in staff half spaces) that this accidental occupies above its position.
        /// </para>
        /// </value>
        public int SpaceAbove
        {
            get
            {
                var isOnLine = (FindOwner<Note>().StaffPosition % 2) == 0;

                switch (Type)
                {
                    case AccidentalType.DoubleFlat:
                    case AccidentalType.Flat:
                        if (isOnLine) return 3;
                        else return 4;
                    case AccidentalType.Sharp:
                    case AccidentalType.Neutral:
                    case AccidentalType.DoubleNeutral:
                        if (isOnLine) return 3;
                        else return 2;
                    case AccidentalType.DoubleSharp:
                        if (isOnLine) return 1;
                        else return 0;
                    default:
                        return 0;
                }
            }
        }

        /// <summary>
        /// Gets the space occupied by this accidental bellow its position.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The space (in staff half spaces) that this accidental occupies bellow its position.
        /// </para>
        /// </value>
        public int SpaceBellow
        {
            get
            {
                var isOnLine = (FindOwner<Note>().StaffPosition % 2) == 0;

                switch (Type)
                {
                    case AccidentalType.DoubleFlat:
                    case AccidentalType.Flat:
                        if (isOnLine) return 1;
                        else return 0;
                    case AccidentalType.Sharp:
                    case AccidentalType.Neutral:
                    case AccidentalType.DoubleNeutral:
                        if (isOnLine) return 3;
                        else return 2;
                    case AccidentalType.DoubleSharp:
                        if (isOnLine) return 1;
                        else return 0;
                    default:
                        return 0;
                }
            }
        }

        /// <summary>
        /// Gets or sets the type of the accidental.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="AccidentalType"/>
        /// </para>
        /// <para>
        /// The type of the accidental.
        /// </para>
        /// </value>
        public AccidentalType Type
        {
            get { return (AccidentalType)Enum.Parse(typeof(AccidentalType), typeAttribute.Value); }
            set
            {
                typeAttribute.Value = value.ToString();

                RaisePropertyChanged("Type");
            }
        }

        /// <summary>
        /// Gets the value of the accidental.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The numerical value of the accidental equivalent to <see cref="ToneElement.Accidental"/>.
        /// </para>
        /// </value>
        public double Value
        {
            get 
            {
                switch (Type)
                {
                    case AccidentalType.DoubleFlat:
                        return -1.0;
                    case AccidentalType.Flat:
                        return -0.5;
                    case AccidentalType.Sharp:
                        return 0.5;
                    case AccidentalType.DoubleSharp:
                        return 1.0;
                    default:
                        return 0.0;
                }
            }
        }

        /// <summary>
        /// Gets the width of the accidental symbol.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The width (in centimeters) of the accidental symbol.
        /// </para>
        /// </value>
        public double Width
        {
            get 
            {
                double baseSize;

                var staffSpaces = FindOwner<Staff>().Descriptor.Lines - 1;
                if (staffSpaces > 0)
                    baseSize = EffectiveTypeSize / staffSpaces;
                else
                    baseSize = FindOwner<Staff>().SpaceSize;

                if (Type == AccidentalType.DoubleNeutral || Type == AccidentalType.DoubleFlat)
                    return 2 * baseSize;
                else if (Type == AccidentalType.None)
                    return 0;
                else
                    return baseSize;
            }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the value of the <see cref="IsCourtesy"/> property.
        /// </summary>
        private XmlAttribute courtesyAttribute;
        /// <summary>
        /// Holds the value of the <see cref="Position"/> property.
        /// </summary>
        private XmlAttribute positionAttribute;
        /// <summary>
        /// Holds the value of the <see cref="Type"/> property.
        /// </summary>
        private XmlAttribute typeAttribute;

        #endregion
    }

    /// <summary>
    /// Describes the type of an accidental.
    /// </summary>
    public enum AccidentalType
    {
        /// <summary>
        /// No accidental.
        /// </summary>
        None,
        /// <summary>
        /// Neutral (cancels a flat or a sharp).
        /// </summary>
        Neutral,
        /// <summary>
        /// Double-neutral (cancels a double-flat or a double-sharp).
        /// </summary>
        DoubleNeutral,
        /// <summary>
        /// Double-flat (lowers the tone by two semitones).
        /// </summary>
        DoubleFlat,
        /// <summary>
        /// Flat (lowers the tone by one semitone).
        /// </summary>
        Flat,
        /// <summary>
        /// Sharp (raises the tone by one semitone).
        /// </summary>
        Sharp,
        /// <summary>
        /// Double-sharp (raises the tone by two semitones).
        /// </summary>
        DoubleSharp
    }
}
